Skip to content

Extract key and flag handling into pure, testable functions - #441

Open
jrr wants to merge 1 commit into
devfrom
claude/pure-key-and-flag-handling
Open

Extract key and flag handling into pure, testable functions#441
jrr wants to merge 1 commit into
devfrom
claude/pure-key-and-flag-handling

Conversation

@jrr

@jrr jrr commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Last of the three test-focused PRs (#439, #440). Two behaviours were only reachable by writing bytes to stdin or by running the CLI, so neither had tests. Both are now ordinary functions.

keyToActionsrc/key-actions.ts

Maps a keypress to a GameAction, or undefined for keys the game ignores. Escape stays in ui.tsx, since quitting the app isn't a game action.

GameAction had to move to types.ts first. Otherwise the new module and ui.tsx would import each other. That also removes the existing reducer.tsui.tsx cycle (finding 2.2), since the reducer now takes GameAction from types.tssrc/ui.js has no importers left besides cli.tsx.

The extraction is not purely mechanical. useInput ran its checks as four independent ifs, so ctrl+Q dispatched input-letter 'Q' and give-up, and any ctrl chord typed its letter into the row. Returning a single action forces a resolution — ctrl chords are checked first and are never letters. The stray letter was invisible before because the loss screen hides the current row, so no user-visible behaviour changes.

parseFlagssrc/cli-options.ts

Turns meow's flags into App's props, or into a message explaining why the game can't start. cli.tsx keeps the process.exit and console.log; only the decision moved, which takes cli.tsx from 73 lines to 51.

A regression from #439, fixed here

While testing the flag path I found that #439 broke --num-boards 0. It replaced opts?.numBoards || 1 with numBoards ?? 1, and ?? does not catch 0 — so the game reached newGame with zero boards and crashed on the first render, where it used to fall back to one board. meow also hands back NaN for a non-numeric value, which broke the same way.

Confirmed against a clean worktree at 6be12e7, before any of these PRs:

flag before #439 after #439 now
--num-boards 0 1 board crash 1 board
--num-boards abc (NaN) 1 board crash 1 board
--num-boards -3 crash crash crash (unchanged)

Normalising with || inside parseFlags restores the old behaviour and puts it somewhere a test can reach. The negative case is untouched and recorded as it.fails for finding 1.3.

Verified by mutation

Mutation Caught by
ctrl check moved after the letter check all three ctrl-chord tests
normalisation reverted to ?? "treats zero boards as one", "treats a non-numeric count as one board"

Checks

yarn build, yarn lint, yarn prettier --check ., the --test midgame --quit smoke run and the --test bogus error path all pass. 36 → 59 passing plus 4 expected failures.

Where the review stands after these three

Done: reducer coverage, invariants, GameAction cycle, expectEqual argument order, and the two extractions.

Still open, and the natural next step: finding 1.1 — the multi-board guess limit. Three it.fails tests are already sitting on it and will flip to passing the moment guessesUsed moves onto GameState. After that, 1.3 (validate the flags, one it.fails waiting) and 1.2 (floor the blank-row count).


Generated by Claude Code

Two behaviours were only reachable by writing bytes to stdin or by
running the CLI, so neither had tests. Both are now ordinary functions.

keyToAction (src/key-actions.ts) maps a keypress to a GameAction, or
undefined for keys the game ignores. Escape stays in ui.tsx, since
quitting the app isn't a game action. This required moving GameAction
from ui.tsx to types.ts first, otherwise the new module and ui.tsx would
import each other; that also removes the existing reducer -> ui.tsx
cycle (finding 2.2), since the reducer now takes GameAction from types.

The extraction is not purely mechanical. useInput ran its checks as four
independent ifs, so ctrl+Q dispatched input-letter 'Q' *and* give-up, and
any ctrl chord typed its letter. Returning a single action forces a
resolution: ctrl chords are checked first and are never letters. The
stray letter was invisible before because the loss screen hides the
current row, so no user-visible behaviour changes here.

parseFlags (src/cli-options.ts) turns meow's flags into App's props, or
into a message explaining why the game can't start. cli.tsx keeps the
process.exit and console.log; only the decision moved.

This also fixes a regression from the earlier newGame change. That
replaced `opts?.numBoards || 1` with `numBoards ?? 1`, and `??` does not
catch 0, so `--num-boards 0` crashed on the first render where it used to
fall back to one board. meow also yields NaN for a non-numeric value,
which broke the same way. Normalising with `||` in parseFlags restores
the old behaviour and puts it somewhere testable. A negative count still
crashes, unchanged, and is recorded as it.fails for finding 1.3.

Verified by mutation: reordering the ctrl check after the letter check,
and reverting the normalisation to `??`, are each caught by the tests
covering that behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EoMUcnWHajEBC54SvGaox
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants